﻿using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Script.Serialization;
using Lilium.Reflection;

namespace Lilium.Web.Mvc.State
{
    /// <summary>
    /// State service which uses controller's temp data to save state.
    /// </summary>
    /// <remarks>
    /// Для храниния данных используется TempData.
    /// Для каждого котроллера генерируется отдельная запись.
    /// </remarks>
    public sealed class TempDataStateService : ControllerService, IStateService
    {
        public TempDataStateService(ControllerBase controller)
            : base(controller)
        {
        }

        public override void OnResultExecuted()
        {
            if (StatefulObjects.Count != 0)
            {
                var stateDictionary = new Dictionary<string, object>();
                foreach (var registratiron in StatefulObjects.Values)
                {
                    var statefulObject = registratiron.StatefulObject;
                    var state = statefulObject.GetState(null);
                    if (state != null)
                        stateDictionary.Add(statefulObject.Key, state);
                }
                Controller.TempData[StateDictionaryDataKey] = stateDictionary;
            }
        }

        #region IStateService Members

        public string GetStateParameterName(IStatefulObject consumer, string propertyName)
        {
            if (consumer == null) throw new ArgumentNullException("consumer");

            return consumer.Key + "." + propertyName;
        }

        public bool PropertyLoaded(IStatefulObject consumer, string propertyName)
        {
            StatefulObjectRegistration registration;
            if (StatefulObjects.TryGetValue(consumer.Key, out registration) && (registration.StatefulObject == consumer))
            {
                return (registration.IsLoadedFromControllerState)
                    || (registration.IsLoadedFromValueProvider)
                    || (registration.LoadedProperties.Contains(propertyName));
            }
            else
                return false;
        }

        public void RegisterConsumer(IStatefulObject statefulObject, bool overrideExistentRegistration)
        {
            if (statefulObject == null) throw new ArgumentNullException("statefulObject");

            if (!StatefulObjects.ContainsKey(statefulObject.Key) || overrideExistentRegistration)
            {
                var registration = new StatefulObjectRegistration(statefulObject);
                LoadFromControllerState(statefulObject, registration);
                LoadFromRequestParameters(statefulObject, registration);
                StatefulObjects[statefulObject.Key] = registration;
            }
            else
                throw new InvalidOperationException(string.Format("State consumer with key {0} already registered.", statefulObject.Key));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="stateDictionary"></param>
        /// <remarks>
        /// Используеся при генерации ссылок, например.
        /// </remarks>
        public void SaveObjectsState(IDictionary<string, object> stateDictionary)
        {
            if (stateDictionary == null) throw new ArgumentNullException("stateDictionary");

            foreach (var registratiron in StatefulObjects.Values)
            {
                var state = registratiron.StatefulObject.GetState(null);
                var pair = SerializeState(registratiron.StatefulObject, state);
                stateDictionary[pair.Key] = pair.Value;
            }
        }

        public void SaveObjectState(IStatefulObject consumer, string actionName, IDictionary<string, object> routeValues)
        {
            if (consumer == null) throw new ArgumentNullException("consumer");
            if (routeValues == null) throw new ArgumentNullException("routeValues");

            var state = consumer.GetState(actionName);
            var kvp = SerializeState(consumer, state);
            routeValues[kvp.Key] = kvp.Value;
        }


        public KeyValuePair<string, object> SerializeState(IStatefulObject consumer, object state)
        {
            if (consumer == null) throw new ArgumentNullException("consumer");

            if (state != null)
                return new KeyValuePair<string, object>(consumer.Key, Serializer.Serialize(state));
            else
                return new KeyValuePair<string, object>(consumer.Key, null);
        }

        #endregion

        private void LoadFromControllerState(IStatefulObject statefulObject, StatefulObjectRegistration registration)
        {
            if (statefulObject == null) throw new ArgumentNullException("statefulObject");

            IDictionary<string, object> stateDictionary = (IDictionary<string, object>)Controller.TempData[StateDictionaryDataKey];
            if (stateDictionary != null)
            {
                object state;
                if (stateDictionary.TryGetValue(statefulObject.Key, out state) && (state != null))
                {
                    statefulObject.SetState(state);
                    registration.IsLoadedFromControllerState = true;
                }
            }
        }

        private void LoadFromRequestParameters(IStatefulObject statefulObject, StatefulObjectRegistration registration)
        {
            if (statefulObject == null) throw new ArgumentNullException("statefulObject");

            object state;

            var stateValueProvider = Controller.ValueProvider.GetValue(statefulObject.Key);
            if (stateValueProvider != null)
            {
                var @params = new Dictionary<string, object>();
                @params["input"] = stateValueProvider.AttemptedValue;
                state = ApplyStateParameters(
                    statefulObject,
                    Serializer.Deserialize(stateValueProvider.AttemptedValue, statefulObject.StateType),
                    registration);
                registration.IsLoadedFromValueProvider = true;
            }
            else
                state = ApplyStateParameters(statefulObject, null, registration);

            if (state != null)
                statefulObject.SetState(state);
        }

        private object ApplyStateParameters(IStatefulObject statefulObject, object currentState, StatefulObjectRegistration registration)
        {
            if (statefulObject == null) throw new ArgumentNullException("statefulObject");

            if (Controller.ValueProvider.ContainsPrefix(statefulObject.Key))
            {
                if (currentState == null)
                    currentState = statefulObject.GetState(null);

                if (currentState == null)
                    currentState = Activator.CreateInstance(statefulObject.StateType);

                foreach (var propertyInfo in currentState.GetType().GetProperties())
                {
                    var propertyValueProvider = Controller.ValueProvider.GetValue(statefulObject.Key + "." + propertyInfo.Name);
                    if (propertyValueProvider != null)
                    {
                        var propertyValue = propertyValueProvider.ConvertTo(propertyInfo.PropertyType);
                        propertyInfo.SetValue(currentState, propertyValue, null);
                        registration.LoadedProperties.Add(propertyInfo.Name);
                    }
                }
            }

            return currentState;
        }

        #region private string TempDataKey

        private string m_StateDictionaryDataKey;

        private string StateDictionaryDataKey
        {
            get
            {
                if (string.IsNullOrEmpty(m_StateDictionaryDataKey))
                {
                    m_StateDictionaryDataKey = this.GetType().Name + ":" + Controller.GetType().AssemblyQualifiedName;
                }
                return m_StateDictionaryDataKey;
            }
        }

        #endregion

        #region private JavaScriptSerializer Serializer

        private readonly JavaScriptSerializer m_Serializer = new JavaScriptSerializer();

        private JavaScriptSerializer Serializer
        {
            get
            {
                return m_Serializer;
            }
        }

        #endregion

        #region private IDictionary<IStatefulObject, StatefulObjectRegistration>

        private readonly IDictionary<string, StatefulObjectRegistration> m_StatefulObjects = new Dictionary<string, StatefulObjectRegistration>(); //TODO: регистрировать информацию о свойствах

        private IDictionary<string, StatefulObjectRegistration> StatefulObjects
        {
            get
            {
                return m_StatefulObjects;
            }
        }

        #endregion

        #region private sealed class StatefulObjectRegistration

        private sealed class StatefulObjectRegistration
        {
            public StatefulObjectRegistration(IStatefulObject statefulObject)
            {
                if (statefulObject == null) throw new ArgumentNullException("statefulObject");

                m_StatefulObject = statefulObject;
            }

            #region public bool IsLoadedFromControllerState

            private bool m_IsLoadedFromControllerState;

            public bool IsLoadedFromControllerState
            {
                get { return m_IsLoadedFromControllerState; }
                set { m_IsLoadedFromControllerState = value; }
            }

            #endregion

            #region public bool IsLoadedFromValueProvider

            private bool m_IsLoadedFromValueProvider;

            public bool IsLoadedFromValueProvider
            {
                get { return m_IsLoadedFromValueProvider; }
                set { m_IsLoadedFromValueProvider = value; }
            }

            #endregion

            #region public HashSet<string> LoadedProperties

            private readonly HashSet<string> m_LoadedProperties = new HashSet<string>();

            public HashSet<string> LoadedProperties
            {
                get
                {
                    return m_LoadedProperties;
                }
            }

            #endregion

            #region public IStatefulObject StatefulObject

            private readonly IStatefulObject m_StatefulObject;

            public IStatefulObject StatefulObject
            {
                get
                {
                    return m_StatefulObject;
                }
            }

            #endregion
        }

        #endregion
    }
}